Skip to main content

03 - 压缩与裁剪

前置01 篇的五个去向、02 篇的腐烂形态。

本篇回答:已经在上下文里的东西怎么砍掉。两种砍法的语义完全不同,代价也完全不同。

本篇会用到的词

意思
压缩(compaction)把旧内容总结成一段摘要,用摘要替换原文。语义保留,细节丢失
裁剪(context editing)直接删掉特定内容块,替换成一句占位文本。不做总结,删了就是删了
compaction 块压缩产生的一个内容块,装着摘要。它必须被回传,否则压缩状态丢失
采样迭代一次 API 调用内部可能发生多次模型采样。压缩就是一次额外的采样迭代
占位文本裁剪后留在原位的一句说明,告诉模型"这里原本有内容,已被移除"

一、两种砍法

同样是"上下文太长了要砍",两者砍掉的东西和留下的东西完全不同压缩 · compact_20260112前 30 轮160K token额外一次模型采样摘要3.5K token+ 决策、未解的 bug、实现细节这类"结论"能留下来+ 服务端自动做,不需要客户端写总结代码− 要多花一次完整的模型调用,输入是全部历史− 有损且不可逆:摘要没提到的细节永久消失裁剪 · clear_tool_uses_20250919第 3 到 20 次工具结果 90K直接删除不调模型占位文本几十 token+ 不花模型调用,省得最彻底+ 粒度精确:能指定哪些工具永不清理− 完全丢失:模型只知道"这里原本有东西"− 清早了会让模型重复调用同一个工具两者是两套独立的 beta(compact-2026-01-12context-management-2025-06-27),混用一个 beta 头会被拒。
官方的定位是:绝大多数长会话场景用服务端压缩,裁剪留给需要精细控制的场景 —— 尤其是重工具调用的 Agent,那里旧的工具结果在模型处理完之后就真的没用了,总结它们反而是浪费。

二、服务端压缩

2.1 机制

const response = await client.beta.messages.create({
betas: ["compact-2026-01-12"],
model: "claude-opus-5",
max_tokens: 16000,
messages,
context_management: {
edits: [{ type: "compact_20260112" }],
},
});

// ⚠️ 必须追加完整的 response.content,不能只取里面的文本块。
// 压缩产生的 compaction 块就在 content 里,API 靠它在下一次请求时
// 丢弃摘要之前的全部内容。只 append 文本 = 压缩状态静默丢失,
// 表现是"开了压缩但上下文还在无限增长",且不报任何错。
messages.push({ role: "assistant", content: response.content });

流程是四步:检测到输入 token 达到阈值 → 生成摘要 → 产生一个 compaction 块 → 带着压缩后的上下文继续这次回答。后续请求里,API 自动丢弃 compaction 块之前的所有内容块。

2.2 全部参数

参数类型默认值说明
typestring必填必须是 "compact_20260112"
triggerobject{"type": "input_tokens", "value": 150000}何时触发。input_tokens 是唯一支持的类型,value 最低 50,000
pause_after_compactionbooleanfalse生成摘要后是否暂停,交回控制权
instructionsstringnull自定义总结提示词。提供时完全替换默认提示词,不是追加

2.3 三个坑

三个都不报错,只有查监控或读文档才会发现① 只回传了文本append 的是 text 而不是 contentcompaction 块丢了现象:开了压缩但上下文照涨查法:看历史里有没有 compaction 块② 工具打断了摘要请求里带了 tools 时模型偶尔在内部总结步骤里去调工具现象:compaction 块 content 为 null修法:用 instructions 明说不许调工具③ 成本统计漏了顶层 input_tokens 不含压缩迭代要遍历 usage.iterations 求和现象:账单远高于自己算出来的量级见下图
第②个坑的官方修法是给 instructions 一段明确禁止调工具的提示词。注意 instructions 是完全替换默认提示词的,所以自定义的那段里必须同时写清"要在摘要里保留什么",否则会连默认提示词里的那些要求一起丢掉。

第③个坑的量级值得单独看一眼。官方文档给的示例响应:

同一次 API 调用,两种统计口径按 usage.input_tokens23,000按 iterations 求和203,000其中压缩那次迭代自己就读了 180,000压缩迭代的输入是"压缩发生前的完整历史",也就是最长的那一刻。它一定是这次调用里最大的一笔。官方文档写得很直白:顶层的 input_tokens / output_tokens 只是所有非压缩迭代之和,要算总账必须遍历 iterations。用压缩来省钱的团队,如果监控口径没改,会同时得到"token 下降了"和"账单上涨了"两个互相矛盾的结论。
另有一条限制值得在算账时一起考虑:总结用的是你请求里指定的那个模型,没有换成便宜模型的选项。用 Opus 跑的 Agent,摘要也是 Opus 生成的。

三、工具结果裁剪

对重工具调用的 Agent,这是比压缩更对症的手段 —— 01 篇那张构成图里,第 40 轮 82% 的占用都在工具结果上。

const response = await client.beta.messages.create({
betas: ["context-management-2025-06-27"],
model: "claude-opus-5",
max_tokens: 16000,
tools,
messages,
context_management: {
edits: [{
type: "clear_tool_uses_20250919",
// 触发阈值:可以按 input_tokens 也可以按 tool_uses 计
trigger: { type: "input_tokens", value: 80000 },
// 保留最近几次工具调用/结果对。留太少会让模型重复调同一个工具
keep: { type: "tool_uses", value: 5 },
// 至少要清掉这么多才执行 —— 否则不值得打破前缀缓存(06 篇)
clear_at_least: { type: "input_tokens", value: 20000 },
// 这些工具的结果永不清理:小而关键、清了就得重调的那些
exclude_tools: ["read_project_config", "get_current_task"],
// 只清结果、保留 Claude 当初的调用参数(默认行为)。
// 设成 true 会连参数一起清,模型会看不出自己调过什么,容易重复调用
clear_tool_inputs: false,
}],
},
});

全部配置项:

配置项默认说明
trigger100,000 输入 token何时激活。可按 input_tokenstool_uses 指定
keep3 次工具调用清理后保留最近几对工具调用/结果,最旧的先删
clear_at_least每次至少要清掉这么多 token,否则本次不执行
exclude_tools这些工具的调用与结果永不被清
clear_tool_inputsfalse是否连工具调用参数一起清。默认只清结果

3.1 clear_at_least 是为缓存存在的

官方文档在讲缓存那一节直接点明了:工具结果裁剪会让缓存前缀失效,所以要"清掉足够多的 token,让这次缓存失效变得值得"。

算术很简单:缓存读取约是常规输入价格的 1/10,缓存写入约 1.25 倍。清掉 2,000 token 却让 80,000 token 的前缀重新走缓存写入,是净亏。clear_at_least 就是把这个判断交给 API 去做。

没设这个参数是最常见的配置错误 —— 默认值是"无",也就是每次达到阈值就清,不管清得值不值。

3.2 exclude_tools 该放什么

判据是"清掉之后模型会不会重新调一遍":

  • 该排除:项目配置、当前任务描述、用户身份这类小而关键的结果。它们只有几百 token,但清掉之后模型会重新调用,反而更贵
  • 不该排除:文件内容、搜索结果这类大块头。它们正是要清的对象

四、思考块裁剪

clear_thinking_20251015 管的是扩展思考产生的 thinking 块。它有一个特别容易踩的地方:默认行为按模型档次不同

模型档次保留全部历史思考只保留最后一轮的思考
OpusClaude Opus 4.5 及之后Claude Opus 4.1 及之前
SonnetClaude Sonnet 4.6 及之后Claude Sonnet 4.5 及之前
Haiku(无)到 Claude Haiku 4.5 为止的全部型号

官方的建议很明确:如果你的代码会跨多个模型档次运行,就显式设置 keep,不要依赖各档的默认值。否则同一份代码在不同模型上的上下文行为不一样,排查时会非常难受。

思考块裁剪和缓存的关系与工具结果裁剪相反:思考块被保留时缓存是保住的,被清理时才在清理点失效。所以 keep 这个参数实际上是在"缓存命中率"和"上下文空间"之间做取舍。

五、阈值怎么定

以 1M 窗口为例,按当前输入量分三档 —— 不要等窗口快满了才开始动手(02 篇:是梯度不是悬崖)< 30% 窗口什么都不做先把工具定义治了(05 篇)这个区间里砍东西省的钱不如破缓存亏的多30% 到 60%开工具结果裁剪配 clear_at_least 与 exclude_tools先砍最没用的那部分不动对话历史> 60%叠加服务端压缩trigger 设在 60% 那个位置留出 20% 到 30% 余量给突发的大工具结果两者可以同时开:裁剪先把工具结果清掉,压缩再处理剩下的对话历史。顺序上裁剪应该更早触发,因为它不花钱。
压缩的触发阈值不要贴着窗口上限设。压缩本身要读一遍完整历史,如果那时候历史已经逼近窗口,这次压缩调用自己就可能超限。默认 150,000 在 1M 窗口下相当保守,正是这个考虑。

六、什么时候不该压缩

  • 需要精确回溯的任务。压缩是有损的,审计、合规、法务场景下摘要不能替代原文。这类场景该走卸载:原文存到外部,上下文里只留索引
  • 单轮或少轮任务。压缩的阈值都在几万 token 以上,短任务永远触发不到,开了只是多一个配置项
  • 对首字延迟极敏感的场景。触发压缩的那一轮会多一次完整的模型采样,用户会感觉到一次明显的卡顿。可以用 pause_after_compaction 把控制权拿回来,在自己这边给个提示
  • 成本敏感且工具结果占大头的场景。这时候裁剪比压缩合适得多 —— 总结一堆早就用完的文件内容,是花钱买一份没人会看的摘要

七、小结

  • 压缩留语义丢细节、要多花一次完整模型调用;裁剪直接删、不花钱但信息全失。重工具调用的 Agent 优先用裁剪
  • messages.push 必须追加完整的 response.content,只取文本会让压缩状态静默丢失
  • 请求里带 tools 时压缩可能失败(compactioncontent: null),要用 instructions 明确禁止调工具
  • 顶层 input_tokens 不含压缩迭代,算成本必须遍历 usage.iterations —— 否则会同时看到"token 下降"和"账单上涨"
  • clear_at_least 是为缓存存在的,不设它等于每次达到阈值就无条件破一次缓存
  • 跨模型档次运行时,思考块裁剪的 keep 必须显式设置
  • 需要精确回溯、单轮任务、首字延迟敏感这三种情况不该用压缩

下一篇:04 - 卸载到外部,第二类手段 —— 不砍,挪出去。

← 回到 专题索引